<?php

/**
 * View.php
 * 
 * 视图渲染类
 * 
 * @author wang <wang@fvzone.com> 
 * @copyright Copyright (c) 2010 Fire vision Software Studio
 * @link http://www.fvzone.com
 * @license New BSD License.{@link http://www.opensource.org/licenses/bsd-license.php}
 * @version $Id: View.php 1.1 2011-8-12 22:48:01Z wang $
 * @package core
 * @since 1.0
 */

defined('IN_WORK') or exit('Authorization error');

class View extends Work
{ 
    /**
     * @var string 部件的名称
     */
    public $widget;

    /**
     * @var string 视图布局
     */
    public $layout; 

    /**
     * @var string 视图主题风格名称(theme)
     */
    public $theme; 

    /** 
     * @var string 视图目录
     */
    public $view_dir = '';

    /**
     * @var string 缓存目录
     */
    public $cache_dir = '';

    /** 
     * @var string 编译目录
     */
    public $compile_dir = '';

    /**
     * @var array 视图变量
     */
    public $_data = array(); 

    /**
     * @var string 模板文件后缀名称
     */
    public $suffix = '.html';

    /**
     * @var int 缓存更新时间, 默认 3600 秒
     */
    protected $lifetime = 3600;

    /**
     * @var bool 直接输出，就是不缓存
     */
    protected $output = false;

    /**
     * @var bool 直接加载PHP视图文件，不解析也不编译，直接返回视图内容
     */
    protected $direct = false; 

    /**
     * @var array 模板文件列表
     */
    protected $template = array();

    /**
     * @var bool 强制编译
     */
    protected $force = true;

    /** 
     * @var string 静态化模板编号
     */
    protected $cache_id = null;

    /**
     * @var int 过期时间
     */
    protected $expires = 0;

    /**
     * @var int 当前时间
     */
    protected $_nowtime = null;

    /** 
     * @var string 当前编译文件
     */
    public static $compile_file = null;

    /**
     * 构造函数
     * 初始化运行环境,或用于运行环境中基础变量的赋值
     * 
     * @return boolean
     */
    public function __construct()
    { 
        //视图开始执行时间
        Work::time('VIEW_START_TIME');
        //定义视图基础目录，并且检查是否开启了主题功能
        $this->view_dir = empty($this->theme) ? PUBLIC_DIR . 'views' . DS : APP_DIR . 'themes/' . $this->theme . DS; 
        //编译目录
        $this->compile_dir = CACHE_DIR . 'components/' . Request::component() . '/views/compile' . DS; 
        //静态缓存目录
        $this->cache_dir = CACHE_DIR . 'components/' . Request::component() . '/views/cache' . DS; 
        //当前时间
        $this->_nowtime = time(); 
        //都项目中的视图配置文件,如果缺少此文件视图功能无法使用
        $params = Config::load('view');

        foreach($params as $key => $value)
        {
            $this->$key = isset($this->$key) ? $value : $this->$key;
        }
    }

    /**
     * 设置视图的主题
     * 
     * @param string $theme_name 所要设置的网页模板主题名称
     * @return string 视图的主题名
     */
    public function setTheme($name = 'default')
    {
        return $this->view_dir = APP_DIR . 'themes/' . $name . DS;
    }

    /**
     * 设置视图文件布局结构的文件名(layout)
     * 
     * @param string $layout_name 所要设置的layout名称
     * @return string layout名称
     */
    public function setLayout($name = null)
    {
        return $this->layout = $name;
    }

    /**
     * 分析视图文件名
     * 
     * @param mixed $file_name 视图名
     * @return stirng 
     */
    protected function parse_file($_name = null)
    { 
        //当视图类在 widget 下运行时
        if ($this->widget)
        {
            $name = is_null($_name) ? $this->widget : $_name;
        }
        else
        { 
            //当参数为空时，默认当前controller当前action所对应的视图文件
            if (is_null($_name))
            { 
                //生成默认视图文件
                $name = Request::controller() . '/' . Request::action();
            }
            else
            { 
                //分析视图文件里是否调用非当前controller的视图文件
                if (strpos($_name, '/') !== false)
                {
                    $name_array = explode('/', $_name);
                    $name = trim($name_array[0]) . '/' . trim($name_array[1]);
                }
                else
                { 
                    //当视图类在widget里运行时
                    $name = Request::controller() . '/' . $_name;
                }
            }
        }

        return $name;
    }

    /**
     * 显示视图文件
     * 
     * @param string $file_name 视图名
     * @return void 
     */
    public function display($file = null, $cache_id = null)
    { 
        //判断layout视图变量
        if ($this->layout)
        {
            $layout_file = 'layout/' . $this->layout; 
            //检查文件是否正常
            $layout_state = is_file($this->view_dir . $layout_file . $this->suffix) ? true : false;
        }
        else
        {
            $layout_state = false;
        } 
        //分析视图文件名
        $view_file = $this->parse_file($file);

        if ($layout_state == false)
        { 
            //加载编译缓存文件
            echo $this->fetch($view_file, $cache_id);
        }
        else
        {
            //获取视图编译文件内容
            $content = $this->fetch($view_file, $cache_id);
            $this->set('content', $content);
            echo $this->fetch($layout_file, $cache_id);
        }

        if (Work::config()->SHOW_TRACE == true)
        {
            echo Trace::show();
        }
    }

    /**
     * 注册变量
     * 
     * @param mix $key 
     * @param mix $value 
     * @return void 
     */
    function set($key, $value = null)
    {
        if (is_array($key))
        {
            foreach ($key as $k => $v)
            {
                if ($k)
                {
                    $this->_data[$k] = $v;
                }
            }
        }
        else
        {
            if ($key)
            {
                $this->_data[$key] = $value;
            }
        }
    }

    /**
     * 执行视图内容赋值操作
     *
     * @param string $_eval_content 视图内容
     * @return string
     */
    function _eval($_eval_content)
    { 
        //视图变量
        if (!empty($this->_data))
        {
            extract($this->_data, EXTR_PREFIX_SAME, 'data');
        }

        ob_start();
        eval('?>' . trim($_eval_content));
        $_eval_content = ob_get_contents();
        ob_end_clean();

        return $_eval_content;
    }

    /**
     * 处理模板文件
     * 
     * @param string $view_name 
     * @param string $cache_id
     * @return sring 
     */
    function fetch($view_name, $cache_id = null)
    {
        self::$compile_file = $view_name;

        //视图运行在部件下
        if ($this->widget)
        {
            $view_file = COMPONENT_DIR . Request::component() . '/widgets/views/' . $view_name . $this->suffix;
        }
        else
        {
            $view_file = COMPONENT_DIR . Request::component() . '/views/' . $view_name . $this->suffix;
        }

        //应用里找视图文件
        if (!is_file($view_file))
        {
            $view_file = $this->view_dir . $view_name . $this->suffix;
        }

        //直接返回视图数据
        if ($this->output)
        { 
            //如果视图文件后缀是php，那就不过滤了直接赋值变量并返回
            if ($this->direct == true)
            {
                $out = $this->_eval(file_get_contents($view_file));
            }
            else
            {
                $out = $this->_eval($this->fetch_str(file_get_contents($view_file)));
            }
        }
        else
        { 
            //读静态文件
            if (!empty($cache_id))
            {
                $out = $this->read_cache($view_file, $cache_id);
            }

            //编译视图文件
            if (empty($out))
            {
                $out = $this->_eval(file_get_contents($this->compile($view_file))); 
                //生成静态缓存
                if ($cache_id)
                {
                    $this->write_cache($view_file, $out, $cache_id);
                }
            }

            if (!in_array($view_file, $this->template))
            {
                $this->template[] = $view_file;
            }
        } 
        //返回html数据
        return $out;
    }

    /**
     * 处理字符串函数
     * 
     * @param string $source
     * @return sring 
     */
    private function fetch_str($source)
    { 
        //替换smarty注释
        $source = preg_replace('/<!--[^>|\n]*?({.+?})[^<|{|\n]*?-->/', '\1', $source); 
        //替换不换行的html注释
        $source = preg_replace('/<!--[^<|>|{|\n]*?-->/', '', $source);
        //删除php标签
        $source = preg_replace("/<\?[^><]+\?>/i", "", $source); 
        //提取{}中的数据
        return preg_replace("/{([^\}\{\n]*)}/e", "\$this->detect('\\1');", $source);
    }

    /**
     * 编译模板函数
     * 
     * @param string $view_file 
     * @return sring 编译后文件地址
     */
    private function compile($view_file)
    {
        $compile_file = $this->compile_dir . self::$compile_file . '.tpl.php';
        $compile_dir = dirname($compile_file);
        self::$compile_file = $compile_file;

        //创建目录
        if (!is_dir($compile_dir))
        {
            mkdir($compile_dir, 0777, true);
        } 
        //过期时间
        if ($this->expires)
        {
            $expires = $this->expires - $this->lifetime;
        }
        else
        {
            if (is_file($compile_file))
            {
                $filestat = stat($compile_file);
            }
            $expires = isset($filestat['mtime']) ? $filestat['mtime'] : 0;
        }

        $filestat = stat($view_file);

        if ($filestat['mtime'] <= $expires && $this->force == false)
        {
            if (!is_file($compile_file))
            {
                $expires = 0;
            }
        }

        if ($this->force || $filestat['mtime'] > $expires)
        {
            $source = $this->fetch_str(file_get_contents($view_file));
            if (file_put_contents($compile_file, $source, LOCK_EX) === false)
            {
                throwException('can\'t write:' . $compile_file);
            }
        }
        return $compile_file;
    }

    /**
     * 判断是否缓存
     * 
     * @param string $view_file 
     * @param string $cache_id 
     * @return bool 
     */
    private function read_cache($view_file, $cache_id = null)
    {
        $cachename = basename($view_file, strrchr($filename, '.')) . '_' . $cache_id;

        if ($cache_id == null)
            return false;

        $hash_dir = $this->cache_dir . substr(md5($cachename), 0, 1);
        $cache_file = $hash_dir . '/' . $cachename . '.php';
        if (is_file($cache_file))
        {
            $data = file_get_contents($cache_file);
            $data = substr($data, 13);
            $pos = strpos($data, '<');
            $paradata = substr($data, 0, $pos);
            $para = unserialize($paradata);

            if ($para === false || $this->_nowtime > $para['expires'])
            {
                $this->cache_id = null;
                return false;
            }
            $this->_expires = $para['expires'];

            $out = substr($data, $pos);

            foreach ($para['template'] AS $val)
            {
                $stat = @stat($val);
                if ($para['maketime'] < $stat['mtime'])
                {
                    $this->cache_id = null;
                    return false;
                }
            }
            return $out;
        }
        $this->cache_id = null;
        return false;
    }

    /**
     * 模板数据写入静态缓存
     *
     * @param string $view_file 
     * @param string $out 
     * @param string $cache_id 
     * @return void 
     */
    private function write_cache($view_file, $out, $cache_id)
    {
        $cachename = basename($view_file, strrchr($view_file, '.')) . '_' . $cache_id;
        $data = serialize(array('template' => $this->template, 'expires' => $this->_nowtime + $this->lifetime, 'maketime' => $this->_nowtime));
        $out = str_replace("\r", '', $out);

        while (strpos($out, "\n\n") !== false)
        {
            $out = str_replace("\n\n", "\n", $out);
        }

        $hash_dir = $this->cache_dir . substr(md5($cachename), 0, 1);
        if (!is_dir($hash_dir))
        {
            mkdir($hash_dir, 0777, true);
        }
        if (file_put_contents($hash_dir . '/' . $cachename . '.php', '<?php exit;?>' . $data . $out, LOCK_EX) === false)
        {
            throwException('can\'t write:' . $hash_dir . '/' . $cachename . '.php');
        }
        $this->template = array();
    }

    /**
     * 处理{}标签
     * 
     * @param string $tag 
     * @return sring 
     */
    private function detect($tag)
    {
        //记录foreach类型
        static $foreach_type;

        $tag = stripslashes(trim($tag));

        if (empty($tag))
        {
            return '{}';
        } 
        //注释部分
        elseif ($tag{0} == '*' && substr($tag, -1) == '*')
        {
            return '';
        } 
        //变量
        elseif ($tag{0} == '$')
        {
            return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
        } 
        //结束 tag
        elseif ($tag{0} == '/')
        {
            switch (substr($tag, 1))
            {
                case 'if':
                    return '<?php endif; ?>';
                    break;

                case 'foreach':
                    if ($foreach_type == 'foreachelse')
                    {
                        $output = '<?php endif; ?>';
                    }
                    else
                    {
                        $output = '<?php endforeach; endif; ?>';
                    }
                    return $output;
                    break;

                case 'literal':
                    return '';
                    break;

                default:
                    return '{' . $tag . '}';
                    break;
            }
        }
        else
        {
            $tag_sel = array_shift(explode(' ', $tag));
            switch ($tag_sel)
            {
                case 'if':
                    return $this->_compile_if_tag(substr($tag, 3));
                    break;

                case 'else':
                    return '<?php else: ?>';
                    break;

                case 'elseif':
                    return $this->_compile_if_tag(substr($tag, 7), true);
                    break;

                case 'foreachelse':
                    $foreach_type = 'foreachelse';
                    return '<?php endforeach; else: ?>';
                    break;

                case 'foreach':
                    $foreach_type = 'foreach';
                    return $this->_compile_foreach_start(substr($tag, 8));
                    break;

                case 'assign':
                    $t = $this->get_para(substr($tag, 7));

                    if ($t['value']{0} == '$')
                    { 
                        //如果传进来的值是变量，就不用用引号
                        $assign = '$this->assign(\'' . $t['var'] . '\',' . $t['value'] . ');';
                    }
                    else
                    {
                        $assign = '$this->assign(\'' . $t['var'] . '\',\'' . addcslashes($t['value'], "'") . '\');';
                    }
                    return '<?php ' . $assign . ' ?>';
                    break;

                case 'include':
                    $t = $this->get_para(substr($tag, 8));

                    return '<?php echo $this->fetch(' . "'$t[file]'" . '); ?>';
                    break;

                case 'widget':
                    $t = $this->get_para(substr($tag, 7));

                    return '<?php $this->widget(' . "'$t[file]'" . '); ?>';
                    break;

                case 'literal':
                    return '';
                    break;

                case 'php':
                    return '<?php ' . substr($tag, 4) . '; ?>';
                    break;

                case 'cycle' :
                    $t = $this->get_para(substr($tag, 6));
                    return '<?php echo $this->cycle(' . $this->make_array($t) . '); ?>';
                    break;

                default:
                    return '{' . $tag . '}';
                    break;
            }
        }
    }

    /**
     * 处理smarty标签中的变量标签
     * 
     * @param string $val 
     * @return bool 
     */
    private function get_val($val)
    {
        if (strrpos($val, '[') !== false)
        {
            $val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val);
        }
        if (strrpos($val, '|') !== false)
        {
            $moddb = explode('|', $val);
            $val = array_shift($moddb);
        }

        if (empty($val))
        {
            return '';
        }

        if (strpos($val, '.$') !== false)
        {
            $all = explode('.$', $val);

            foreach ($all AS $key => $val)
            {
                $all[$key] = $key == 0 ? $this->make_var($val) : '[' . $this->make_var($val) . ']';
            }
            $p = implode('', $all);
        }
        else
        {
            $p = $this->make_var($val);
        }

        if (!empty($moddb))
        {
            foreach ($moddb AS $key => $mod)
            {
                $s = explode(':', $mod);
                switch ($s[0])
                {
                    case 'escape':
                        $s[1] = trim($s[1], '"');
                        if ($s[1] == 'html')
                        {
                            $p = 'htmlspecialchars(' . $p . ')';
                        }elseif ($s[1] == 'url')
                        {
                            $p = 'urlencode(' . $p . ')';
                        }elseif ($s[1] == 'decode_url')
                        {
                            $p = 'urldecode(' . $p . ')';
                        }elseif ($s[1] == 'quotes')
                        {
                            $p = 'addslashes(' . $p . ')';
                        }elseif ($s[1] == 'u8_url')
                        {
                            $p = 'urlencode(' . $p . ')';
                        }
                        else
                        {
                            $p = 'htmlspecialchars(' . $p . ')';
                        }
                        break;

                    case 'nl2br':
                        $p = 'nl2br(' . $p . ')';
                        break;

                    case 'default':
                        $s[1] = $s[1]{0} == '$' ? $this->get_val(substr($s[1], 1)) : "'$s[1]'";
                        $p = 'empty(' . $p . ') ? ' . $s[1] . ' : ' . $p;
                        break;

                    case 'truncate':
                        $p = 'sub_str(' . $p . ",$s[1])";
                        break;

                    case 'strip_tags':
                        $p = 'strip_tags(' . $p . ')';
                        break;

                    default: 
                        # code...
                        break;
                }
            }
        }
        return $p;
    }

    /**
     * 处理去掉$的字符串
     * 
     * @param string $val 
     * @return bool 
     */
    private function make_var($val)
    {
        if (strrpos($val, '.') === false)
        {
            $p = '$' . $val;
        }
        else
        {
            $t = explode('.', $val);
            $_var_name = array_shift($t);
            $p = '$' . $_var_name;

            foreach ($t AS $val)
            {
                $p .= "['{$val}']";
            }
        }
        return $p;
    }

    /**
     * 处理insert外部函数/需要include运行的函数的调用数据
     * 
     * @param string $val 
     * @param int $type 
     * @return array 
     */
    private function get_para($val)
    {
        $pa = $this->str_trim($val);
        foreach ($pa AS $value)
        {
            if (strrpos($value, '='))
            {
                list($a, $b) = explode('=', str_replace(array(' ', '"', "'", '&quot;'), '', $value));
                if ($b{0} == '$')
                {
                    $para[$a] = $this->get_val(substr($b, 1));
                }
                else
                {
                    $para[$a] = $b;
                }
            }
        }

        return $para;
    }

    /**
     * 处理if标签
     * 
     * @param string $tag_args 
     * @param bool $elseif 
     * @return string 
     */
    private function _compile_if_tag($tag_args, $elseif = false)
    {
        preg_match_all('/\-?\d+[\.\d]+|\'[^\'|\s]*\'|"[^"|\s]*"|[\$\w\.]+|!==|===|==|!=|<>|<<|>>|<=|>=|&&|\|\||\(|\)|,|\!|\^|=|&|<|>|~|\||\%|\+|\-|\/|\*|\@|\S/', $tag_args, $match);

        $tokens = $match[0]; 
        //make sure we have balanced parenthesis
        $token_count = array_count_values($tokens);
        if (!empty($token_count['(']) && $token_count['('] != $token_count[')'])
        {
            throwException('unbalanced parenthesis in if statement');
        }

        for ($i = 0, $count = count($tokens); $i < $count; $i++)
        {
            $token = &$tokens[$i];
            switch (strtolower($token))
            {
                case 'eq':
                    $token = '==';
                    break;

                case 'ne':
                case 'neq':
                    $token = '!=';
                    break;

                case 'lt':
                    $token = '<';
                    break;

                case 'le':
                case 'lte':
                    $token = '<=';
                    break;

                case 'gt':
                    $token = '>';
                    break;

                case 'ge':
                case 'gte':
                    $token = '>=';
                    break;

                case 'and':
                    $token = '&&';
                    break;

                case 'or':
                    $token = '||';
                    break;

                case 'not':
                    $token = '!';
                    break;

                case 'mod':
                    $token = '%';
                    break;

                default:
                    if ($token[0] == '$')
                    {
                        $token = $this->get_val(substr($token, 1));
                    }
                    break;
            }
        }

        if ($elseif)
        {
            return '<?php elseif (' . implode(' ', $tokens) . '): ?>';
        }
        else
        {
            return '<?php if (' . implode(' ', $tokens) . '): ?>';
        }
    }

    /**
     * 处理foreach标签
     * 
     * @param string $tag_args 
     * @return string 
     */
    private function _compile_foreach_start($tag_args)
    {
        $attrs = $this->get_para($tag_args);

        $from = $attrs['from'];
        $item = '$' . $attrs['item'];

        if (!empty($attrs['key']))
        {
            $key = '$' . $attrs['key'] . ' => ';
        }
        else
        {
            $key = null;
        }

        $output = "<?php if (count($from)): \$n = 0; foreach ($from as $key$item): \$n++; ?>";
        return $output;
    }


    private function str_trim($str)
    { 
        //处理'a=b c=d k = f '类字符串，返回数组
        while (strpos($str, '= ') != 0)
        {
            $str = str_replace('= ', '=', $str);
        }
        while (strpos($str, ' =') != 0)
        {
            $str = str_replace(' =', '=', $str);
        }
        return explode(' ', trim($str));
    }

    /**
     * 用于轮转输出一组值
     *
     * @param array $arr 
     * @return string
     */
    private function cycle($arr)
    {
        static $k, $old;

        $value = explode(',', $arr['values']);
        if ($old != $value)
        {
            $old = $value;
            $k = 0;
        }
        else
        {
            $k++;
            if (!isset($old[$k]))
            {
                $k = 0;
            }
        }
        echo $old[$k];
    }

    //制作数组
    private function make_array($arr)
    {
        $out = '';
        foreach ($arr AS $key => $val)
        {
            if ($val{0} == '$')
            {
                $out .= $out ? ",'$key'=>$val" : "array('$key'=>$val";
            }
            else
            {
                $out .= $out ? ",'$key'=>'$val'" : "array('$key'=>'$val'";
            }
        }
        return $out . ')';
    }
}
